查看原文
其他

Go1.22 新特性:Slices 变更 Concat、Delete、Insert 等函数,对开发挺有帮助!

陈煎鱼 脑子进煎鱼了 2024-05-03

大家好,我是煎鱼。

在 Go1.22 这个新版本起,切片(Slices)新增和变更了一些行为。对于开发者相对更友好了一点。

以下涉及 Concat、Delete、DeleteFunc、Replace、Compact、CompactFunc、Insert 等函数的新版本调整的讲解和分享。

新增 Concat 函数

在以前的 Go 版本中,有一个很常见的使用场景,如果我们想要拼接两个切片。必须要手写类似如下的代码:

func main() {
 s1 := []string{"煎鱼""咸鱼""摸鱼"}
 s2 := []string{"炸鱼""水鱼""煎鱼"}

 s3 := append(s1, s2...)
 fmt.Println(s3)
}

输出结果:

[煎鱼 咸鱼 摸鱼 炸鱼 水鱼 煎鱼]

如果在 Go 工程中常用到,大家还会在类似 util 包上补一个这种函数,便于复用。搞不好还要基于不同的数据类型都实现一遍。

可能的实现如下:

func concatSlice[T any](first []T, second []T) []T {
 n := len(first)
 return append(first[:n:n], second...)
}

func main() {
 s1 := []string{"煎鱼""炸鱼"}
 s2 := []string{"水鱼""摸鱼""煎鱼"}
 s3 := concatSlice(s1, s2)

 fmt.Println(s3)
}

输出结果:

[煎鱼 炸鱼 水鱼 摸鱼 煎鱼]

如果要合并超过 2 个的切片,这个函数的实现就更复杂一些。

但是!

在 Go1.22 起,新增了 Concat 函数,可以用于拼接(连接)多个切片。不需要自己维护一个公共方法了。

Concat 函数签名如下:

func Concat[S ~[]E, E any](slices ...S) S

使用案例如下:

import (
 "fmt"
 "slices"
)

func main() {
 s1 := []string{"煎鱼"}
 s2 := []string{"炸鱼""青鱼""咸鱼"}
 s3 := []string{"福寿鱼""煎鱼"}
 resp := slices.Concat(s1, s2, s3)
 fmt.Println(resp)
}

该函数是基于泛型实现的,不需要像以前一样,每个类型都在内部实现一遍。用户使用起来非常方便。但需要确保传入的切片类型都是一致的。

其内部函数实现也比较简单。如下代码:

// Concat returns a new slice concatenating the passed in slices.
func Concat[S ~[]E, E any](slices ...S) S {
 size := 0
 for _, s := range slices {
  size += len(s)
  if size < 0 {
   panic("len out of range")
  }
 }
 newslice := Grow[S](nil, size)
 for _, s := range slices {
  newslice = append(newslice, s...)
 }
 return newslice
}

需要注意的是:当 size < 0 时会触发 panic。但我感觉这更多只是一个防御性的编程处理。一般情况下不会被触发。

变更 Delete 等函数行为结果

Go1.22 起,将对会缩小切片片段/大小的相关函数的结果行为进行调整,切片经过缩小后新长度和旧长度之间的元素将会归为零值

将会涉及如下函数:Delete、DeleteFunc、Replace、Compact、CompactFunc 等函数。

以下是一些具体的案例。分为旧版本(Go1.21)、新版本(Go1.22 及以后)。

Delete 相关函数

旧版本:

func main() {
 s1 := []int{11121314}
 s2 := slices.Delete(s1, 13)
 fmt.Println("s1:", s1)
 fmt.Println("s2:", s2)
}

输出结果:

s1: [11 14 13 14]
s2: [11 14]

新版本程序不变,运行结果发生了改变,输出结果为:

s1: [11 14 0 0]
s2: [11 14]

Compact 函数

旧版本:

func main() {
 s1 := []int{1112121215}
 s2 := slices.Compact(s1)
 fmt.Println("s1:", s1)
 fmt.Println("s2:", s2)
}

输出结果:

s1: [11 12 15 12 15]
s2: [11 12 15]

新版本程序不变,运行结果发生了改变,输出结果为:

s1: [11 12 15 0 0]
s2: [11 12 15]

Replace 函数

旧版本:

func main() {
 s1 := []int{11121314}
 s2 := slices.Replace(s1, 1399)
 fmt.Println("s1:", s1)
 fmt.Println("s2:", s2)
}

输出结果:

s1: [11 99 14 14]
s2: [11 99 14]

新版本程序不变,运行结果发生了改变,输出结果为:

s1: [11 99 14 0]
s2: [11 99 14]

变更 Insert 函数行为,可能会 panic

旧版本:

func main() {
 s1 := []string{"煎鱼""炸鱼""水鱼"}
 s2 := slices.Insert(s1, 4)
 fmt.Println("s1:", s1)
 fmt.Println("s2:", s2)
}

输出结果:

s1: [煎鱼 炸鱼 水鱼]
s2: [煎鱼 炸鱼 水鱼]

新版本程序不变,运行结果发生了改变,输出结果为:

panic: runtime error: slice bounds out of range [4:3]

goroutine 1 [running]:
slices.Insert[...]({0xc00010e0c0?, 0x10100000010?, 0x7ecd5be280a8?}, 0xc00010aee8?, {0x0?, 0x60?, 0x10052e4c0?})
 ...

以上场景是使用 slices.Insert 函数下,以前没有填入具体要插入的元素,是会正常运行的。在新版本起,会直接导致 panic。

当然,如果一开始就有填入。无论是新老版本,都会导致 panic。相当于是修复了一个边界值了。

一点质疑

可能会有同学说,这不对劲啊。Go1 不是有兼容性保障吗?这么多函数的行为变更,是可以这么干的吗?



从官方文档角度来看是可以的,因为其强调了其当前文档并未承诺将过时元素归零或不归零。也就是没有承诺不变,但承诺过可能产生变更。

总结

今天我们针对切片(Slices)的各函数变更和新增进行了实际的讲解和案例分享。整体来看,还是基于泛型做的修修补补,虽然不是大功能特性。但是对于我们实际做 Go 工程开发的同学来讲,这是比较实在的。

推荐阅读


关注和加煎鱼微信,

一手消息和知识,拉你进技术交流群👇



你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路

日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!


原创不易 点赞支持

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存